Repository navigation
[None][feat] GEN slot preemption: pause decode requests in place to admit at-risk waiters - #21334
reasonsolo wants to merge 22 commits into
Conversation
…ests in place On a saturated disagg GEN worker every max_num_active_requests slot is taken, so a waiting request whose TTFT is about to miss the GEN admission target can only wait for a request to finish. This adds opt-in slot preemption: when no slot is free and the request the admission queue pops next has at most TLLM_GEN_PREEMPT_SLACK_S of slack left against the auto policy's target X (X - (age + s2f) in [0, slack]), a running decode request is paused and the freed slot admits the waiter. A paused request is paused in place: it stays resident (KV, KDA/Mamba state, draft KV, DSpark rolling window and sequence slot are kept), it is only left out of the scheduled batch and out of the admission count. So a pause costs no copy and no extra step, and the scheduled batch (and every CUDA graph) stays within max_batch_size. The pools holding one entry per resident request reserve TLLM_GEN_PREEMPT_MAX_PAUSED more entries: the sequence-slot pool, the V2 KV index pool, the V2 recurrent-state pool (and its memory estimate) and the DSpark rolling-window slots. Paused requests resume ahead of any new admission, oldest first, as slots free up, and after TLLM_GEN_PREEMPT_MAX_PAUSE_S regardless. TLLM_GEN_PREEMPT_POLICY picks the victim: newest (tail order), wait (longest resident), tps (only requests above TLLM_GEN_PREEMPT_TARGET_TPS since their first token, also over the expected pause; largest margin first) or long (most KV). Guardrails: one pause per request, 2 s run time since the first token, one pause per pass, near-finished requests kept. Every decision uses rank-identical inputs only (active requests, token and decode-iteration counts, the broadcast queue and rank 0's clock), so it also runs under helix. The existing helix allocation-failure error stays. Off by default (policy off, max paused 0): no pool grows and nothing changes. Needs the auto GEN admission policy; disabled with one info log otherwise, or under attention DP / PP, or when a pool is short. Rank 0 logs a stat line every 30 s. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
… in place GEN slot preemption pauses a decode request in place: it stays resident but is left out of the scheduled batch, and its id reaches the spec metadata as paused_in_place_ids. The DSv4 DSpark rolling-window path already keeps such a request's window, but the standalone DSpark worker (DSparkWorker) and DFlash run DFlashSpecMetadata.prepare, which frees the drafter context slot of every request missing from the batch. A paused request then lost its slot, context length and chunk position, and drafted after resume from an empty context like a fresh disagg GEN arrival. - DFlashSpecMetadata.prepare keeps the slots of paused_in_place_ids; a request that finishes after a pause (neither batched nor paused) is still released. - DFlashWorker sizes its request slots like the other per-resident pools: max_batch plus gen_preemption.resident_headroom() (0 when preemption is off, so nothing changes by default), with the dummy slot after them. The private ctx arena, the per-slot context lengths and the block-table rows follow num_slots; _batch_to_slot stays per batch row. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
…MPT_LATE_TRIGGER) GEN slot preemption pauses a running decode request only when the GEN admission queue's next pop is in the slack tier with slack against the adaptive target X in [0, TLLM_GEN_PREEMPT_SLACK_S]. A waiter that is already late (slack < 0), or a next pop from the guard or breaker tier, never triggers a pause, so under sustained overload the trigger goes quiet exactly when the queue is furthest behind. TLLM_GEN_PREEMPT_LATE_TRIGGER=1 drops the lower bound and the tier condition: the next pop is at risk whenever it is keyed and its slack is at most SLACK_S, late or not, and num_at_risk counts every keyed waiter in that range. The trigger stays head-only and the per-pass budget is unchanged. Default 0 keeps the current behaviour. The flag is a GenPreemptionConfig field, so the startup "enabled" line shows it. Meant to run with TLLM_GEN_PREEMPT_MAX_PER_REQUEST=2. Tests: late and guard-tier next pops trigger only with the flag, an on-time next pop with too much slack never does; a request is paused twice through the executor pass (resume lane, hysteresis) and its third pause is refused at max_per_request=2; the startup line shows the knobs. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
…et_output_tps) and an adaptive TPOT-budget controller Replace the TLLM_GEN_PREEMPT_* environment knobs with scheduler_config.gen_preemption (prototype) and derive every other parameter online. Config: set at most one of - tpot_budget: allowed mean-TPOT increase from pauses, in (0, 1); - target_output_tps: per-request output tokens/s to keep (> 0), converted to budget = max(0, R0 / target - 1) with R0 the measured unpaused per-request rate (GenPreemptionSettings.budget). Nothing is paused at a zero budget. Neither: off (no pool grows). Both: validation error. Pipeline parallelism and attention DP are rejected in TorchLlmArgs validation; PP is checked again at executor setup with a ValueError. A missing auto GEN admission policy or KV cache manager V2 scheduler disables preemption with one warning. Policy (sim phase 21): with D the running mean decode time and R0 = 1 / the running mean TPOT of completed requests (pauses excluded, 500 completions), - at-risk window g x 0.15 D; g in [0.25, 2] is an integral controller, one step per completion once the admission target X is set, time constant 3000 completions, holding the mean TPOT inflation from pauses at 0.8 x budget; - victim: the longest resident request, ties newest (victim_key); the newest / tps / long policies are removed; - paused cap 0.375 x max_num_active_requests (also the pool headroom); forced resume after 0.35 D, minimum decode 0.07 D (10 s / 2 s until the first completion); late trigger always on; 2 pauses per request, 1 per pass. Inputs stay rank-identical (completions are requests leaving the active list, timed on rank 0's broadcast clock); no new collective. Pools are sized from the settings installed at executor creation (gen_preemption.set_active_settings) instead of the environment; the executor gets its settings through the PyExecutor constructor. The 30 s rank-0 stat line adds g, the window, D, R0, the budget and the TPOT inflation of the window's completions. Telemetry manifest and the disaggregated-serving docs cover the two fields. Tests: settings, exclusivity and bounds, the R0 / target conversion, pool headroom, wait victim order and zero-budget gating, guardrails, controller (warm start, fixed budget, one step per completion, no update before X or without a budget, clamps, convergence from g = 0.25 / 1 / 2 on a toy plant), executor pass and setup (PP rejection), llm_args round trip and validation. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
…objective (laxity victims, sacrifice credit) target_output_tps no longer converts to a TPOT budget (max(0, R0 / T - 1)). It is now a service-level objective: T is a per-request output-rate floor that at least 1 - eps of the requests meet (eps = 0.2, an internal constant). tpot_budget keeps the phase-21 controller and wait victims unchanged. Policy (k3-disagg-sim phase 24, SIMULATION 6.25): - laxity L = t_first + (max_tokens - 1) / T - (now + remaining / r), r the request's own unpaused rate; - victim: the largest L among requests with L >= 0.02 D and < 8 pauses, resumed when L is down to 0.01 D (or after 0.35 D); else a request already below T (L < 0 or sacrificed) with < 3 pauses, longest resident; else, with credit >= 1, sacrifice the longest resident (credit -= 1); - credit per completion after X: clamp(credit + eps - [below T] + [sacrificed], +-3000 eps); - resume lane: earliest forced-resume time first (least laxity); - window fixed at 0.15 D (no controller), cap 0.375 x max_num_active_requests, late trigger on. All inputs are rank-identical (token counts, max_tokens, completions, rank 0's broadcast clock); no new collective. The 30 s rank-0 stat line adds below_target (window / total), credit, laxity / below-target / sacrificed victims, mean laxity, laxity resumes and paused below-target requests. Sim, 16 seeds vs off, T = control / 1.10: c220 DWDP16 p90 -2.69 s, TPOT +5.7%, 12% of requests below T; DEP16 -2.02 s / +4.6% / 12%. c250 DWDP16 -3.51 s / +8.1%, DEP16 -3.35 s / +8.5%. Total tps within +-0.5%. E2E p90 0.3-1.9 s worse than tpot_budget at c220-c250. Not GPU-tested. Tests: tpot_budget tests no longer go through the conversion; new target_output_tps tests for laxity, victim classes and pause limits, the laxity-margin forced resume, the credit bucket, least-laxity resume order, no controller, the stat line, an executor pass and rank-identical inputs. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
…m the change Same user-facing config (tpot_budget / target_output_tps) and the same decisions; the change is about half its previous size. - gen_preemption.py: one per-request state object, one GenPreemption.step() entry point per scheduling pass (observe, resume, pause), the two modes as two victim strategies over shared pause/resume bookkeeping, a single resume lane ordered by due time, a short stat line, and executor setup checks (create_gen_preemption) moved out of PyExecutor. - The late trigger is always on (it was never configurable), so GenAdmissionWaitingQueue.num_at_risk drops its flag. - Pools add resident_headroom() unconditionally (it is 0 when preemption is off, and pipeline parallelism is rejected when it is on). - Shorter llm_args descriptions and docs; focused, parametrized tests. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
…e two user fields GenPreemptionSettings now holds only tpot_budget / target_output_tps; the policy constants are class attributes of GenPreemption and the paused cap is a module function. No behavior change. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
…ption-pr Conflicts: PyExecutor.__init__ and SchedulerConfig fields (kept both sides); test_gen_preemption.py follows main's tests/unittest/_torch/executor -> tests/unittest/_torch/pyexecutor move. Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #81955 [ run ] triggered by Bot. Commit: |
Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
|
Automatically added "ci: full pre-merge approved" because this PR has satisfied the required GitHub review approvals. Unresolved review conversations and other required checks remain independent merge requirements. |
|
/bot run --disable-fail-fast |
|
PR_Github #82791 [ run ] triggered by Bot. Commit: |
|
PR_Github #82791 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #82946 [ run ] triggered by Bot. Commit: |
|
PR_Github #82946 [ run ] completed with state
|
Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #83037 [ run ] triggered by Bot. Commit: |
|
PR_Github #83037 [ run ] completed with state
|
Signed-off-by: Lizhi Zhou <1432185+reasonsolo@users.noreply.github.com>
|
/bot run --disable-fail-fast |
|
PR_Github #83189 [ run ] triggered by Bot. Commit: |
|
PR_Github #83189 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #83216 [ run ] triggered by Bot. Commit: |
Description
Adds opt-in GEN slot preemption on top of the TTFT-percentile admission queue from #20736. When every decode slot is occupied and a waiter risks missing its TTFT target, the controller pauses a resident decode request and admits the waiter.
tpot_budgetprefers the longest resident request and controls mean pause-induced TPOT inflation.target_output_tpsprefers requests with output-rate slack, then requests already below target, then bounded sacrifices funded by completion credit.Configuration
Set exactly one field on the GEN worker; neither means disabled:
Requires the auto GEN admission policy (
0 < TLLM_GEN_ADMISSION_TTFT_PERCENTILE < 99) and the V2 KV scheduler. Pipeline parallelism and attention DP are rejected. Startup and DFlash's config-time fit check exclude headroom when prerequisites disable preemption.Pausing releases batch slots, not KV pages. Size caches for active plus paused requests; full-attention demand still depends on sequence lengths. GEN workers cannot recompute context, and Helix CP raises on decode allocation failure. Pool sizing assumes one executor per process.
Performance evidence
Earlier adaptive-controller GPU run: Kimi-K3, 32 GB300 GPUs, DWDP16 context, Helix-CP16 GEN, AgentX c220, batch 128, 3600 s:
tpot_budget: 0.10The measured controller inflation was 7.7% against its 8% setpoint. These results predate the review fixes and compaction; no new GPU performance run was performed.
target_output_tpsis simulation-validated only.Validation
Focused coverage retains admission/victim boundaries, controller accounting and limits, output-rate slack/credit, rank-identical decisions, startup prerequisites, forced-resume displacement accounting, drafter retention/reclamation, PEFT residency, and cache sizing. Existing cache tests are parameterized rather than duplicated. Config/schema validation and the mixed-precision transfer regression remain included.